package main

import (
	"fmt"
	"math/rand"
)

func main() {
	/*
		1.堆内存 与 栈内存：
		（1）Go 程序会在 2 个地方为变量分配内存，一个是全局的堆空间来动态分配内存，另一个是每个 goroutine 的栈空间。

		（2）在函数上申请一个变量，
			如果分配在栈中，函数执行结束时自动回收；
			如果分配在堆中，函数结束后某个时间点进行垃圾回收。

		（3）在 栈 上分配和回收内存的开销很低，只需要 2 个 CPU 指令：PUSH 和 POP
			PUSH：将数据 push 到栈空间以完成分配
			 POP：释放空间
			在栈上分配内存，消耗的仅是将数据拷贝到内存的时间，而内存的 I/O 通常能够达到 30GB/S，因此在栈上分配内存效率是非常高的。

		（4）在 堆 上分配内存，一个很大的二外开销则是 垃圾回收。
			Go语言使用的是 标记清除算法，并且在此基础上使用了 三色标记法 和 写屏障技术，提高了效率。

			标记清除器：是跟踪式垃圾收集器，其执行过程可分为 标记（Mark）和 清除（Sweep）两个阶段:
				标记阶段：从根对象出发查找并标记堆中所有存活的对象。（典型耗时，需要暂停程序，标记结束之后，用户程序才能继续执行）
				清除阶段：遍历堆中的全部对象，回收未被标记的垃圾对象并将回收的内存加入空闲列表。

		2.逃逸分析：
		（1）什么是逃逸分析：
			C语言中，可以使用 malloc 和 free 手动在堆上分配和回收内存。
			Go语言中，内存是通过垃圾回收机制自动管理的。

			逃逸分析：编译器决定内存分配位置的方式。
				由编译器完成，作用于编译阶段。

		（2）指针逃逸：
			在函数中创建了一个对象，返回了这个对象的指针。
				这种情况下，函数虽然退出了，但是因为指针的存在，对象的内存不能随着函数结束而回收，因此只能分配在堆上。

		（3）interface{} 动态类型逃逸：
			Go语言中，空接口（interface{}）可以表示任意的类型，如果函数参数为 interface{}，在编译期间很难确定其参数的具体类型，也会发生逃逸。

		（4）栈空间不足：
			操作系统对内核线程使用的栈空间是有大小限制的，64位系统上通常是 8 MB。

			因为栈空间通常比较小，因为递归函数实现不当时，容易导致栈溢出。

			对于Go语言来说，运行时（runtime）尝试在 goroutine 需要的时候动态地分配栈空间，goroutine 的初始栈大小为 2KB.
			当 goroutine 被调度时，会绑定内核线程执行，栈空间大小也不会超过操作系统的限制。

			对于Go语言来说，超过一定大小的局部变量将逃逸到堆上，不同的 Go 版本的大小限制可能不一样。

		（5）闭包：
			一个函数和其对周围状态的引用捆绑在一起（或者说函数被引用包围），这样的组合就是闭包。
			闭包让我们可以在一个内层函数中访问到其外层函数的作用域。

		3.如何利用逃逸分析提升性能
			传值 VS 传指针
		（1）传值 会拷贝整个对象，而 传指针 只会拷贝指针地址，指向的对象是同一个。
		（2）传指针 可以减少值的拷贝，但是会导致内存分配逃逸到堆中，增加垃圾回收（GC）的负担。
			在对象频繁创建和删除的场景下，传递 指针 导致的 GC 开销可能会严重影响性能。
		（3）一般情况下，对于 ☞需要修改原对象值☜，或 ☞占用内存比较大的结构体☜，选择 传指针。
			对于 ☞只读的 占用内存较小☜ 的结构体，直接 传值 能够获得更好的性能。☃

	*/
	fmt.Println("----------------------指针逃逸------------------------------")
	demo := createDemo("demo")
	fmt.Println(demo)

	fmt.Println("---------------------interface{}逃逸--------------------------")
	/*
		1.
			demo 是 main函数中的一个局部变量，该变量作为实参传递给 fmt.Println()，
			但是因为 fmt.Println() 的参数类型定义为 interface{}，因此也发生了逃逸。
	*/
	fmt.Println(demo)

	/*
		2.
		func test(demo *Demo) {
			fmt.Println(demo.name)
		}
		这种情况下，局部变量 demo 不会发生逃逸，但是 demo.name 仍旧会逃逸。
	*/
	test(demo)

	fmt.Println("-------------------------栈空间不足---------------------------")
	/*
		当切片占用内存超过一定大小，或无法确定当前切片长度时，对象占用内存将在堆上分配。
			下面的代码中：
			make([]int, 8191)发生逃逸
			make([]int, 8192) 和 make([]int, n)逃逸到堆上

	*/
	generate8191() //创建了大小为 8191 的 int 型切片，恰好小于 64 KB（64位机器上，int占8字节），不包含切片内部字段占用的内存大小
	generate8192() //创建了大小为 8192 的 int 型切片，恰好占用 64 KB。
	generate(1)    //generate(1)切片大小不确定，调用时传入。

	fmt.Println("--------------------------闭包----------------------------")
	/*
		返回值是一个闭包函数，该闭包函数访问了外部变量n，那变量 n 将会一直存在，直到 in 被销毁。
		变量 n 占用的内存不能随着函数 Increase() 的退出而回收，因此将会逃逸到堆上。
	*/
	in := Increase()
	fmt.Println(in()) //1
	fmt.Println(in()) //2
}

// 指针逃逸
type Demo struct {
	name string
}

func createDemo(name string) *Demo {
	d := new(Demo) //局部变量 d 逃逸到堆
	d.name = name
	return d
}

// interface{} 逃逸
func test(demo *Demo) {
	fmt.Println(demo.name)
}

// 栈空间不足
func generate8191() {
	nums := make([]int, 8191) // < 64KB
	for i := 0; i < 8191; i++ {
		nums[i] = rand.Int()
	}
}

func generate8192() {
	nums := make([]int, 8192) // = 64KB
	for i := 0; i < 8192; i++ {
		nums[i] = rand.Int()
	}
}

func generate(n int) {
	nums := make([]int, n) //不确定大小
	for i := 0; i < n; i++ {
		nums[i] = rand.Int()
	}
}

// 闭包
func Increase() func() int {
	n := 0
	return func() int {
		n++
		return n
	}
}
